const fs = require('fs');
const axios = require('axios');

const INDENT = '  ',
  LINE_SEPARATOR = '\n',
  INLINE_SEPARATOR = ', ',
  PATH = './src/services',
  TYPES_FILE_NAME = 'api-types.ts',
  BACKEND_SERVER = 'http://localhost:8089',
  FUNCTIONS_FILE_NAME = 'api-functions.ts';

axios.get(`${BACKEND_SERVER}/v2/api-docs`).then(({ data }) => {
  if (data) {
    const typeGenerator = new TypeGenerator(data.definitions);
    const functionGenerator = new FunctionGenerator(data.paths);
    const typesFilePath = `${PATH}/${TYPES_FILE_NAME}`;
    if (fs.existsSync(typesFilePath)) {
      fs.unlinkSync(typesFilePath);
    }
    if (!fs.existsSync(PATH)) {
      fs.mkdirSync(PATH);
    }
    fs.writeFile(typesFilePath, typeGenerator.generate().join(LINE_SEPARATOR), (err) => {
      if (err) {
        console.error(err);
      } else {
        console.info(`${TYPES_FILE_NAME} generated`);
      }
    });
    const functionsFilePath = `${PATH}/${FUNCTIONS_FILE_NAME}`;
    if (fs.existsSync(functionsFilePath)) {
      fs.unlinkSync(functionsFilePath);
    }
    fs.writeFile(functionsFilePath, functionGenerator.generate().join(LINE_SEPARATOR), (err) => {
      if (err) {
        console.error(err);
      } else {
        console.info(`${FUNCTIONS_FILE_NAME} generated`);
      }
    });
  }
});

function isValidIdentifier(identifier) {
  return /^[a-zA-Z_$][\w$]*$/.test(identifier);
}

function normalizeIdentifier(identifier) {
  if (isValidIdentifier(identifier)) {
    return identifier;
  } else {
    let normalizedIdentifier = '';
    for (let i = 0, isFirst = true; i < identifier.length; i++) {
      const pattern = isFirst ? /[a-zA-Z_$]/ : /[\w$]/;
      if (pattern.test(identifier[i])) {
        isFirst = false;
        normalizedIdentifier += identifier[i];
      }
    }
    return normalizedIdentifier;
  }
}

const ADMIN_API_PATH_PATTERN = /\/api\/admin\//;
const OPEN_API_PATTERN = /\/api\/open\//;
function isApiPath(path) {
  return ADMIN_API_PATH_PATTERN.test(path) && !OPEN_API_PATTERN.test(path);
}

function extractNameFromOperationId(operationId) {
  return normalizeIdentifier(/^(.*)Using(GET|POST|PUT|DELETE).*$/.exec(operationId)[1]);
}

function capitalizeFirstCharacter(str) {
  return str.substring(0, 1).toUpperCase() + str.substring(1);
}
function extractNamespaceFromTags(tags) {
  return normalizeIdentifier(
    tags[0]
      .split('-')
      .slice(0, -1)
      .map((str) => capitalizeFirstCharacter(str))
      .join(''),
  );
}
function extractTypeNameFromRef(ref) {
  const parts = ref.split(/[\/.]/);
  return parts[parts.length - 1];
}

const METHODS_TO_PARSE = ['get', 'post', 'delete'];

class TypeGenerator {
  constructor(definitions) {
    this.refs = new Map(); // 存放解析后的类型数据
    this.definitions = definitions; // 原始数据
    this.parsing = []; // 解析栈，用于检测循环依赖
    this.aliases = new Map(); // 类型别名，用于解决循环依赖
    Object.keys(definitions).forEach((typeName) => this.getTypeByName(typeName));
  }

  getTypeByName(typeName) {
    if (this.refs.has(typeName)) {
      return this.refs.get(typeName);
    } else if (!isValidIdentifier(typeName)) {
      console.warn(`${typeName} discarded because it is not a valid javascript identifier`);
    } else {
      const typeDefinition = this.definitions[typeName];
      if (typeDefinition === undefined) {
        throw new Error(`cannot find definition for type ${typeName}`);
      } else {
        if (this.parsing.includes(typeName)) {
          // 存在循环依赖，生成别名并保存别名和dummy对象
          console.warn(`circular dependency detected: ${[...this.parsing, typeName]}`);
          const alias = '' + Date.now();
          this.aliases.set(alias, typeName);
          const type = { name: alias, type: 'alias' };
          this.refs.set(alias, type);
          return type;
        } else {
          this.parsing.push(typeName);
          const type = this.parseType(typeDefinition);
          this.parsing.pop();
          this.refs.set(typeName, type);
          return type;
        }
      }
    }
  }

  parseType(typeDefinition) {
    if (typeDefinition.hasOwnProperty('type')) {
      const { type } = typeDefinition;
      if (type === 'integer' || type === 'number') {
        return {
          name: 'number',
          type: 'number',
        };
      } else if (type === 'string') {
        return {
          name: 'string',
          type: 'string',
        };
      } else if (type === 'boolean') {
        return {
          name: 'boolean',
          type: 'boolean',
        };
      } else if (type === 'array') {
        const items = this.parseType(typeDefinition.items);
        return {
          name: `${items.name} array`,
          type: 'array',
          items: items,
        };
      } else if (type === 'object') {
        if (typeDefinition.hasOwnProperty('title')) {
          if (!isValidIdentifier(typeDefinition.title)) {
            return {
              name: 'unknown',
              type: 'unknown',
            };
          }
          const obj = { name: typeDefinition.title, type: 'object' };
          if (typeDefinition.hasOwnProperty('properties')) {
            obj.fields = Object.entries(typeDefinition.properties).map(([name, typeDefinition]) => {
              return {
                fieldName: name,
                fieldType: this.parseType(typeDefinition),
              };
            });
          } else {
            console.warn(`${JSON.stringify(typeDefinition)}: has title but no properties`);
          }
          return obj;
        } else {
          return {
            name: 'any',
            type: 'any',
          };
        }
      } else {
        throw new Error(`${JSON.stringify(typeDefinition)}: unrecognizable type ${type}`);
      }
    } else if (typeDefinition.hasOwnProperty('$ref')) {
      const typeName = extractTypeNameFromRef(typeDefinition.$ref);
      return this.getTypeByName(typeName);
    } else {
      throw new Error(`${typeDefinition} has neither type nor $ref`);
    }
  }

  generate() {
    const lines = [...this.generateBaseInterfaces()];
    for (const typeDefinition of this.refs.values()) {
      if (typeDefinition.type === 'object') {
        const line = this.getTSInterfaceString(typeDefinition);
        lines.push(line);
      }
    }
    return lines;
  }

  generateBaseInterfaces() {
    const lines = [];

    lines.push('export interface Resp<T> {');
    lines.push(`${INDENT}code: 200`);
    lines.push(`${INDENT}message: string`);
    lines.push(`${INDENT}data: T`);
    lines.push('}');

    lines.push(`export type Response<T> = Resp<T>`);
    lines.push(`export type AsyncResponse<T = any> = Promise<Response<T>>`);

    lines.push('export type Long = number');
    lines.push('export type Short = number');
    lines.push('export type Integer = number');
    lines.push('export type String = string');

    return lines;
  }

  getTSInterfaceString(typeDefinition) {
    if (typeDefinition.type !== 'object') {
      throw new Error(`${JSON.stringify(typeDefinition)} cannot be converted into an interface`);
    }
    const lines = [];
    lines.push(`export interface ${typeDefinition.name} {`);
    if (typeDefinition.fields) {
      console.warn(`typeDefinition: ${JSON.stringify(typeDefinition)}`);
      for (const field of typeDefinition.fields) {
        const { fieldName, fieldType } = field;

        lines.push(
          INDENT + `${fieldName}?: ${fieldType ? this.getTSTypeString(field.fieldType) : 'any'}`,
        );
      }
    } else {
      lines.push(INDENT + '[key: string]: any');
    }
    lines.push('}');
    return lines.join(LINE_SEPARATOR);
  }
  getTSTypeString(typeDefinition) {
    const { type } = typeDefinition;
    const PRIMITIVE_TYPES = ['number', 'boolean', 'string', 'any', 'unknown'];
    if (type === 'alias') {
      const actualName = this.aliases.get(typeDefinition.name);
      return this.getTSTypeString(this.getTypeByName(actualName));
    } else if (PRIMITIVE_TYPES.includes(type)) {
      return type;
    } else if (type === 'array') {
      return `${this.getTSTypeString(typeDefinition.items)}[]`;
    } else if (type === 'object') {
      return typeDefinition.name;
    }
  }
}

const TYPE_MAP = new Map([
  ['integer', 'number'],
  ['byte', 'number'],
  ['short', 'number'],
  ['number', 'number'],
  ['boolean', 'boolean'],
  ['string', 'string'],
  ['object', '{ [key: string]: string }'],
]);
class FunctionGenerator {
  constructor(paths) {
    this.paths = paths;
    const functions = Object.entries(paths)
      .filter(([path]) => isApiPath(path))
      .flatMap(([path, obj]) => this.parseEndpoint(path, obj));
    this.services = new Map();
    functions.forEach((func) => {
      let { name, namespace } = func;
      const array = this.services.get(namespace) || [];
      // resolve collision
      const names = array.map((func) => func.name);
      let i = 0;
      while (names.includes(name)) {
        i++;
        name += i;
      }
      this.services.set(namespace, [...array, { ...func, name }]);
    });
  }

  generate() {
    const lines = [...this.generateImports()];

    Array.from(this.services.entries()).forEach(([namespace, functions]) => {
      lines.push(`export const ${namespace || 'Misc'} = {`);
      functions.forEach((func) => lines.push(this.getFunctionString(func)));
      lines.push('}');
    });
    return lines;
  }
  generateImports() {
    const lines = [];
    const EXTRACT_FILE_NAME_PATTERN = /^(.*)\.ts$/;
    const importPath = `./${EXTRACT_FILE_NAME_PATTERN.exec(TYPES_FILE_NAME)[1] || TYPES_FILE_NAME}`;
    lines.push(`import * as types from '${importPath}'`);
    lines.push(`import { AsyncResponse } from '${importPath}'`);
    lines.push("import {RequestOptionsInit} from 'umi-request'");
    lines.push("// @ts-ignore");
    lines.push("import { request } from 'umi'");
    return lines;
  }

  parseEndpoint(path, obj) {
    const methods = [];
    METHODS_TO_PARSE.forEach((httpMethod) => {
      const method = obj[httpMethod];
      if (method && !method.deprecated) {
        methods.push({
          path,
          method: httpMethod,
          name: extractNameFromOperationId(method.operationId),
          namespace: extractNamespaceFromTags(method.tags),
          parameters: method.parameters
            ? method.parameters
                .map((param) => this.parseParameter(param))
                .filter((param) => !!param)
                // 去重，swagger生成的文档json某些情况下param会重复 (/api/uc/v1/accountRemark/delete)
                .filter(
                  (param, index, array) => array.findIndex((p) => p.name === param.name) === index,
                )
            : [],
        });
      }
    });
    if (methods.length > 1) {
      return methods.map((method) => ({
        ...method,
        name: method.method + capitalizeFirstCharacter(method.name),
      }));
    } else {
      return methods;
    }
  }

  parseParameter(obj) {
    const parameter = {
      name: normalizeIdentifier(obj.name),
      place: obj.in,
    };
    if (obj.hasOwnProperty('type')) {
      if (obj.type === 'array') {
        if (obj.hasOwnProperty('items')) {
          parameter.type = this.parseParameter(obj.items).type + '[]';
        } else {
          parameter.type = 'any[]';
        }
      } else {
        const type = TYPE_MAP.get(obj.type);
        if (type === undefined) {
          console.warn(`unknown type: ${obj.type}`);
          parameter.type = 'any';
        } else {
          parameter.type = type;
        }
      }
    } else if (obj.hasOwnProperty('schema')) {
      if (obj.schema.hasOwnProperty('type')) {
        const type = TYPE_MAP.get(obj.schema.type);
        if (type === undefined) {
          console.warn(`unknown type: ${obj.schema.type}`);
          parameter.type = 'any';
        } else {
          parameter.type = type;
        }
      } else if (obj.schema.hasOwnProperty('$ref')) {
        const typeName = extractTypeNameFromRef(obj.schema.$ref);
        if (!isValidIdentifier(typeName)) {
          console.warn(`${typeName} discarded because it is not a valid identifier`);
          return null;
        } else {
          parameter.type = `types.${typeName}`;
        }
      } else {
        throw new Error(`${JSON.stringify(obj)}: no type or $ref`);
      }
    } else {
      throw new Error(`unknown: ${JSON.stringify(obj)}`);
    }
    return parameter;
  }

  getFunctionString(func) {
    const paramList = func.parameters
      .map((param) => `${param.name}?: ${param.type}`)
      .join(INLINE_SEPARATOR);
    const paramNameList = func.parameters.map((param) => param.name).join(INLINE_SEPARATOR);
    const params =
      func.parameters.length > 3 ? `{ ${paramNameList} }: { ${paramList} }` : paramList;

    let path = `\`${func.path}\``;
    func.parameters
      .filter((param) => param.place === 'path')
      .forEach((param) => (path = path.replace(`/{${param.name}}`, `/\${${param.name}}`)));

    const queryString = func.parameters
      .filter((param) => param.place === 'query')
      .map((param) => param.name)
      .join(INLINE_SEPARATOR);
    const query = queryString ? `params: { ${queryString} }` : '';

    const bodyString =
      func.parameters.filter((param) => param.place === 'body').map((param) => param.name)[0] || '';
    const body = bodyString ? `data: ${bodyString}` : '';

    const optionsString = [query, body].filter((str) => str).join(INLINE_SEPARATOR);

    const options = `{ method: '${func.method}', ${
      optionsString ? optionsString + ', ' : ''
    } ...(requestOptions ?? {}) }`;

    const lines = [];
    lines.push(
      INDENT +
        `async ${this.getFunctionName(func)} <T = any>(${
          params ? params + ', ' : ''
        }requestOptions?: RequestOptionsInit): AsyncResponse<T> {`,
    );
    lines.push(INDENT + INDENT + `return request(${path}, ${options})`);
    lines.push(INDENT + '},');
    return lines.join(LINE_SEPARATOR);
  }
  getFunctionName(func) {
    return func.name;
  }
}
